AtCoder Beginner Contest 345 A-F 题解

AtCoder Beginner Contest 345

A - Leftrightarrow

Question

给你一个由 <=> 组成的字符串 S

请判断 S 是否是双向箭头字符串。

字符串 S 是双向箭头字符串,当且仅当存在一个正整数 k ,使得 S 是一个 <k= 和一个 >的连接,且顺序如此,长度为 (k+2)

Solution

按照题意模拟

Code

#include <bits/stdc++.h>
using namespace std;
int main() {
    string s;
    cin >> s;
    if (s[0] != '<') {printf("No\n"); return 0;}
    if (s.back() != '>') {printf("No\n"); return 0;}
    for (int i = 1; i < s.size() - 1; i++) {
        if (s[i] != '=') {printf("No\n"); return 0;}
    }
    printf("Yes\n");
}

B - Integer Division Returns

Quesiton

给定一个介于 10181018 之间的整数 X ,打印 X10
这里, a 表示不小于 a 的最小整数。

Solution

分类讨论,如果 a 能整除 10,那么答案就是 a10

Code

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main() {
    ll n; cin >> n;
    if (n % 10 == 0) {
        cout << n / 10 << '\n';
    }
    else {
        if (n > 0) {
            cout << n / 10 + 1 << '\n';
        }
        else {
            cout << n / 10 << '\n';
        }
    }
}

C - One Time Swap

Question

给你一个字符串 S 。请找出以下操作 1 次的字符串数。

Solution

考虑交换的对数是 n(n+1)2

如果一个字母,和之前相同的字母交换,得到的还是原串,否则得到的串是一个新串

所以我们只需要统计每个字符与之前的多少个字母不同就可以得到答案

注意要判断是否能得到原串

Code

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main() {
    string s; cin >> s;
    ll n = s.size(); s = " " + s;
    vector<ll> num(26,0);
    ll ans = 0, flg = 0;
    for (int i = 1; i <= n; i++) {
        if (num[s[i] - 'a'] > 0) flg = 1;
        num[s[i] - 'a']++;
        ans += i - num[s[i] - 'a'];
    }
    cout << ans + flg << '\n';
}

D - Tiling

Quesiton

有一个由 H 行和 W 列组成的网格,每个单元格的边长为 1 ,我们有 N 块瓷砖。
其中的 i 瓷砖( 1iN )是一个大小为 Ai×Bi 的矩形。
请判断是否有可能将这些瓷砖放置在网格上,从而满足以下所有条件:

N7,H,W10

Solution

考虑到 N,H,W 都特别小,直接暴力

直接枚举放瓷砖的顺序以及每块砖是否旋转,最后暴力放砖即可

我在放砖的时候使用了优先队列,所以时间复杂度为 O(HWlogHW)

实际上判断放砖可以优化到 O(HW)

总时间复杂度就为 O(N!2NHW)

Code

#include <bits/stdc++.h>
using namespace std;

typedef vector<vector<int> > vvi;
typedef pair<int, int> pii;


int main() {
    int n; cin >> n;
    int H, W; cin >> H >> W;
    vector<pii> a(n + 1);
    for (int i = 1; i <= n; i++) {
        cin >> a[i].first >> a[i].second;
    }

    auto check = [&]  (vector<int> &id, int S) {
        vector<vector<int> > v(H + 1, vector<int>(W + 1,0));
        priority_queue<pii, vector<pii>, greater<pii> > pq;
        for (int i = 1; i <= H; i++) {
            for (int j = 1; j <= W; j++) {
                pq.push({i,j});
            }
        }
        for (int i = 1; i <= n; i++) {
            auto &[dx, dy] = a[id[i]];
            if (S >> (i - 1) & 1) 
                swap(dx, dy);
            while (!pq.empty() && v[pq.top().first][pq.top().second]) pq.pop();
            if (pq.empty()) {return 1;}
            auto [x, y] = pq.top(); pq.pop();
            for (int i_ = x; i_ < x + dx; i_++) {
                for (int j_ = y; j_ < y + dy; j_++) {
                    if (i_ > H || j_ > W || v[i_][j_] == 1) return 0;
                    v[i_][j_] = 1;
                }
            }
        }
        while (!pq.empty() && v[pq.top().first][pq.top().second]) pq.pop();
        if (pq.empty()) return 1;
        return 0;
    };

    vector<int> id (n + 1);
    iota(id.begin(), id.end(), 0);
    do {
        for (int S = 0; S < (1<<n); S++) 
            if (check(id, S)) {cout << "Yes\n"; return 0;}  
    } while (next_permutation(id.begin() + 1, id.end()));
    cout << "No\n";
    return 0;
}

E - Colorful Subsequence

Question

N 个球排成一排。
左边的 i 个球的颜色是 Ci ,数值是 Vi

高桥希望在不改变顺序的情况下,将这一行中的 K 个球移除,这样在排列剩余的球时,就不会有相邻的两个球颜色相同。此外,在此条件下,他希望最大化这一行中剩余球的总价值。

请计算高桥是否能移除 K 个球,使剩下的一行中没有相邻的两个球颜色相同。如果可以,求剩余球的最大总值。

Solution

先考虑一种朴素的 DP,定义 dp[i][j][col] 表示前 i 个球,以及移走了 j 个,末尾的最后一个球的颜色为 col 的最大值

考虑两种情况:

这样的时间复杂度为 O(N2K) 会超时

观察发现 dp[i][j1][col] 的很多值都是空的,所以考虑值维护其最大值以及和最大值颜色不同的次大值,这样子每次转移肯定能从最大值和次大值里面挑一个转移,后面的小值就不需要去考虑了

所以我们修改一下定义

dp[i][j][p] 记录前 i 个球,移走了 j 个,的最大值,次大值的 v[i] 总值和颜色

转移过程和朴素情况一样,只是当最大值和当前的 c[i] 相同时,改用次大值转移

这样的空间复杂度为 O(NK) 会超内存

发现 dp[i] 的状态只与 dp[i1] 有关,所以考虑使用滚动数组把空间优化到了 O(K)

Code

#pragma GCC optimize(3)
#pragma GCC optimize("Ofast")
#pragma GCC optimize("unroll-loops")
#pragma GCC target("avx,avx2,fma")
#pragma GCC optimize(2)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<ll,ll> pll;
const ll inf = 1e15;

int main() {
    freopen ("E.in", "r", stdin);
    int k ,n; cin >> n >> k;
    vector<int> c(n + 1), v(n + 1);
    for (int i = 1; i <= n; i++) 
        cin >> c[i] >> v[i];
    
    const vector<pll> infv = {{-inf, -1}, {-inf, -2}};
    vector<vector<vector<pll>>> dp(2);  // 三维数组 dp[i][j][col] 表示前 i 个物品中选了 j 个,且最后一个物品的颜色是 col 的最大价值
    dp[0].resize(k + 1, infv); dp[1].resize(k + 1, infv);
    dp[0][0][0].first = dp[0][0][1].first = 0;

    for (int i = 1; i <= n; i++) {
        const int col = c[i], val = v[i];
        auto &cur = dp[i & 1], &pre = dp[(i - 1) & 1];
        for (int j = 0; j <= k; j++)
            cur[j] = infv;
        
        //选了当前物品
        for (int j = 0; j <= k; j++) {
            for (auto &x : pre[j]) {
                const ll preval = x.first, precol = x.second;
                if (precol == col) continue;
                cur[j].push_back({preval + val, col});
                break;
            }
        }
        //没选当前物品
        for (int j = 1; j <= k; j++) {
            for (auto &x : pre[j - 1]) {
                const ll preval = x.first, precol = x.second;
                cur[j].push_back({preval, precol});
            }
        }

        //去重
        for (int j = 0; j <= k; j++) {
            auto &a = cur[j];
            sort(a.begin(), a.end()); reverse(a.begin(), a.end());
            if (a[0].second == a[1].second) {
                ll secval = -inf, seccol = -2;
                for (auto x : a) {
                    if (x.second != a[0].second) {
                        secval = x.first;
                        seccol = x.second;
                        break;
                    }
                }
                a[1] = {secval, seccol};
            }
            a.resize(2);
        }
    }

    ll ans = dp[n & 1][k][0].first;
    cout << (ans < 0 ? -1 : ans) << endl;
    return 0;
}

F - Many Lamps

Question

有一个简单的图,图中有 N 个顶点,编号为 1N ,有 M 条边,编号为 1M 。边 i 连接顶点 uivi

每个顶点上都有一盏灯。初始时,所有的灯都是熄灭的。

请在 0M 之间(包括首尾两次)执行以下操作,以确定是否可以恰好打开 K 盏灯。

如果可以恰好打开 K 盏灯,请打印实现该状态的操作序列。

Solution

简化问题,整个图是连通的

有一个性质:开的灯的数量肯定是偶数,证明如下

每次对一条边进行操作:

奇偶性不变,初始是 0 为偶数,所以亮的灯的数量总是偶数

考虑连通图的总数为 N ,设 X 为小于等于 N 的最大偶数,有一种构造方法能让 X 盏灯都点亮

为什么这样是有效的,因为如果 u 是灭灯

如果 K<X ,亮灯的过程是从 0X 的连续偶数,当亮灯数 =X 时,中止过程即可

Code

#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> pii;
int main() {
    freopen ("F.in", "r", stdin);
    int n, m, k; cin >> n >> m >> k;
    vector<vector<pii> > g(n + 1);
    for (int i = 1; i <= m; i++) {
        int u,v; cin >> u >> v;
        g[u].push_back({v, i});
        g[v].push_back({u, i});
    }

    int Y = 0;
    vector<int> ans, vis(n + 1, 0), cur(n + 1, 0);
    function<void(int)> dfs = [&](int u) {
        vis[u] = 1;
        for (auto [v, id] : g[u]) {
            if (vis[v]) continue;
            dfs (v);
            if (cur[v] == 0 && Y < k) { 
                Y -= cur[u] + cur[v];
                cur[u] ^= 1; cur[v] ^= 1;
                Y += cur[u] + cur[v];
                ans.push_back(id);
            }
        }
    };

    for (int i = 1; i <= n; i++) 
        if (!vis[i]) dfs(i);
    
    if (Y != k) cout << "No" << endl;
    else {
        cout << "Yes" << endl;
        cout << ans.size() << endl;
        for (auto x : ans) cout << x << " ";
        cout << endl;
    }
    return 0;
}

G - Sugoroku 5

Question

有一个棋盘游戏,其中有 N+1 个方格:方格 0 、方格 1 、方格 和方格 N
你有一个骰子,它能掷出一个介于 1K 之间的整数,每种结果的概率相等。
您从 0 开始掷骰子。重复下面的操作,直到到达 N 格:

假设 Pi 是经过恰好 i 次操作后到达 N 个方格的概率。计算 P1,P2,,PN % 998244353

1KN2×105

Solution

假设 F(x) 是一次投掷筛子前进 n 格的生成函数

F(x)=1Ki=1Kxi

那么, k 掷完后, (0k<N) 位于方格的概率表示为 [xk]Fnn 掷出之后 (0k<N) 的概率表示为 [xk]Fn 。因此, an ,即 n 掷出 (0nN) 后没有达到目标的概率,可以表示为

an=k=0N1[xk]Fn

那么,恰好投掷 n 次后达到目标的概率为 (1an)(1an1)=an1an

所以只需要枚举 a0,a1,,an 就可以知道答案

变换一下式子:

an=k=0N1[xk]Fn=[xN1](1+x++xN1)Fn

考虑按照 K 的大小分类讨论

  1. K 比较大的情况:
an=[xN1](1+x++xN1)Fn=[xN1](1+x++xN1+xN+)Fn=[xN1]Fn1xF(x)=1Ki=1Kxi=x(1xK)K(1x)

在上面的表达式中得出

an=[xN1]xn(1xK)nKn(1x)n+1=1Kn[xN1n](1xK)n×1(1x)n+1

考虑到 (1xK)n 只在,0,K,2K, 阶项中非零,则可以在 O(N/K) 的时间内求出 ana0aN 的时间复杂度为 O(N2/K)

  1. K 比较小的情况
an=[xN1](1+x++xN1)Fn

先考虑利用分治的方法求 an

#include <iostream>
#include <vector>
using namespace std;

#include "atcoder/convolution.hpp"
#include "atcoder/modint.hpp"
using mint = atcoder::modint998244353;

int N, K;
vector<mint> f; // F = 1/K sum{i=1..K} x^i
vector<mint> a; // (a_0, a_1, ..., a_N)

// input  : l, r, g = (sum{i=0..N-1} x^i) F^l mod x^N
// output : F^{r-l} mod x^N
vector<mint> dc(int l, int r, vector<mint> g) {
  int b = g.size();
  if (l + 1 == r) {
    a[l] = g.back();
    return f;
  }
  int m = (l + r) / 2;
  auto p = dc(l, m, g);
  g = atcoder::convolution(g, p), g.resize(b);
  auto q = dc(m, r, g);
  auto res = atcoder::convolution(p, q);
  if ((int)res.size() > N) res.resize(N);
  return res;
}

int main() {
  cin >> N >> K;
  a.resize(N + 1);
  f.resize(K + 1);
  for (int i = 1; i <= K; i++) f[i] = mint{K}.inv();
  dc(0, N + 1, vector<mint>(N, 1));
  for (int i = 0; i < N; i++) cout << (a[i] - a[i + 1]).val() << "\n";
}

总时间复杂度为 O(N2log2N)

我们发现对于一个递归函数 dc(l,r,g) g 最多乘 F (rl+1) 次,那么对于 g 中的小项,对答案没有影响

所以只需要保留最后的 K×(rl+1)+1 项即可

具体代码为:

  int b = min<int>(g.size(), (r - l - 1) * K + 1);
  g.erase(begin(g), begin(g) + g.size() - b);

这样就能在 O(NKlog2N) 的时间复杂度下解决问题了

分界点取 K=N/logN

总时间复杂度为 O(N1.5logN)

好像还有 O(NlogN) 的解法,但是我不会www

Code

//https://atcoder.jp/contests/abc345/submissions/51423091
#pragma GCC optimize(3)
#include<bits/stdc++.h>
using namespace std;
using LL = long long;
#define endl '\n'
using db = double;
template <class T>
using max_heap = priority_queue<T>;
template <class T>
using min_heap = priority_queue<T, vector<T>, greater<>>;
#include "atcoder/convolution.hpp"
#include "atcoder/modint.hpp"
using mint = atcoder::modint998244353;
int n, K, N;
const int D = ((db)sqrt(2e5) / (db)(log(2e5) / log(2)) + 1);
const int maxn = 3e5;
mint fac[maxn + 1], invfac[maxn + 1];
mint binom(int nn, int mm)  //求组合数 C(mm,nn)
{
	if (nn < mm)
		return 0;
	return fac[nn] * invfac[nn - mm] * invfac[mm];
};
void init()
{
	fac[0] = 1;
	for (int i = 1; i <= maxn; ++i)
		fac[i] = fac[i - 1] * i;
	invfac[maxn] = 1 / fac[maxn];
	for (int i = maxn - 1; i >= 0; --i)
		invfac[i] = invfac[i + 1] * (i + 1);
}
void solve1()
{
	vector<mint> f(n + 1);
	mint tot = 1;
	mint inv = 1, invK = mint{K}.inv();
	for (int i = 1; i <= n; ++i)
	{
		tot *= K;
		mint res = 0;
		inv *= invK;
		if (1LL * i * K < 1LL * n)
		{
			f[i] = 0;
			continue;
		}
		for (int j = 0; j <= i; ++j)
		{
			int sgn = (j % 2 == 0) ? 1 : -1;
			if (1LL * n - 1 - 1LL * j * (K) < 0)
				break;
			res += sgn * binom(i, j) * binom(n - 1 - j * (K), i);
		}
		f[i] = (tot - res) * inv;
	}
	for (int i = n; i >= 1; --i)
		f[i] -= f[i - 1];
	for (int i = 1; i <= n; ++i)
		cout << f[i].val() << endl;
}
vector<mint> a, f;
vector<mint> dc(int l, int r, vector<mint> g)
{
	int b = min<int>(g.size(), (r - l - 1) * K + 1); // 只保留最后 (r - l) * K 个
	g.erase(begin(g), begin(g) + g.size() - b);
	if (l + 1 == r)
	{
		a[l] = g.back();
		return f;
	}
	int m = (l + r) / 2;
	auto p = dc(l, m, g);
	g = atcoder::convolution(g, p), g.resize(b);
	auto q = dc(m, r, g);
	auto res = atcoder::convolution(p, q);
	if ((int)res.size() > N) res.resize(N);
	return res;
}
void solve2()
{
	a.resize(n + 1);
	f.resize(K + 1);
	for (int i = 1; i <= K; i++)
		f[i] = mint{K}.inv();
	dc(0, N + 1, vector<mint>(N, 1));
	for (int i = 0; i < n; i++)
		cout << (a[i] - a[i + 1]).val() << "\n";
}
int main()
{
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cin >> n >> K;
	N = n;
	init();
	if (K > D)
		solve1();
	else
		solve2();
	return 0;
}